package nachos.network;
import java.io.ByteArrayInputStream;
import nachos.machine.*;
import java.util.LinkedList;
import nachos.threads.Condition2;
import nachos.threads.Lock;
//NetSocket++++++++++++++++++++++++++++++++++++++++++++++++
// "A glorified queue!"
// It basicly a wraper aound a bytestream from post office.
// Read and Write will be used to send data over the net
// via PostOffoce.send/recieve.  It will implement sliding
// to ensure all packets arive and are in order.  The sliding
// window has an infinite size, but can only acceot win-size
// number of pacets at a time... at least thats how i
// understand it.
// a few wiki articles taht may help:
// sockets
// sliding window
// and definatly TCP.  thatll help a lot
// packets... maybe
//=========================================================
public class NetSocket extends OpenFile {
	//var block---------------
	public static final int windowSize = 16;
	public enum SocketState {	// an enum type.  just a bit of fancy jiberish to keep track of the state  google java enum if you need help
		NStateClosed, NStateClosing, NStateFinSent, NStateFinAckSent, NStateEstablished, NStateSTPsent,
		NStateSTPrcvd, NStateSynSent, NStateSynAckSent, NStateDeadlock
	}
	private SocketState state; //the state of the socket: established, closed, etc
	private int thisPortNum; //the port number on each end of the link "this is me, that is them"
	private int thisMachineNum;
	private int thatPortNum;
	private int thatMachineNum;
	private int connectionid;
	private window swin;
	private int resendCounter;
	private static final int resendCounterMax = 4;
	private window rwin;
	private LinkedList<Byte> readyToWrite;   //the array of packets to be sent by send... may not need it
	private boolean cToggle;  //omg! magic!
	private boolean eToggle;
	//thread controll
	private Lock sendLock;
	private Lock recieveLock;
	//========================
	//Constructor++++++++++++++++++++++++++++++++++++++
	// takes a port number and whether its called from
	// connect or accept. I'll handle this part.  It's
	// the meat of the syscalls so to speak.
	// give it a mchine number and itll act like the sender.
	// give it just a port num, and itll behave as the reciever
	//=================================================
	public NetSocket(int portnum){
		//init simple things
		cToggle = true;
		eToggle = false;
		connectionid = 0;

		//thread controll
		recieveLock = new Lock();
		sendLock = new Lock();

		//gather port information\
		thisPortNum = portnum;
		thisMachineNum = Machine.networkLink().getLinkAddress();
		thatMachineNum = -1;
		thatPortNum = -1;
		state = SocketState.NStateClosed;

		//set up the lists
		readyToWrite = new LinkedList<Byte>();
		swin = new window(windowSize);
		rwin = new window(windowSize);
	}
	//Send and Recieve+++++++++++++++++++++++++++++++++
	// heres an update of what this shit's ment to do.
	// cansend() - just returns whether or not there is
	// data to be sent
	// send() - saves a temp of the first item in the send
	// window, moves it to the back of the window, and returns
	// that item.  that way each call returns the next item
	// in the window
	// sendAll() - sends the entire window through the postOffice
	// it'd probab;y be a good idea to use the send function thats already
	// been writen
	//=================================================
	private MailMessage nextPacket(){
		//varblok--------------
		MailMessage data = null;
		byte[] array;
		//---------------------
		try {
			if(readyToWrite.isEmpty() && !swin.notempty() && state == state.NStateClosing && state != state.NStateFinSent){
				Lib.debug('l', "adding a fin packet");
				state = state.NStateFinSent;
				data = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid, new byte[]{11}, false, false, true, packetToggle());
			} else if (!readyToWrite.isEmpty()){
				array = new byte[java.lang.Math.min(readyToWrite.size(), MailMessage.maxContentsLength)];
				for (int i = 0; i < array.length; i++) {
					array[i] = readyToWrite.removeFirst();
				}
				data = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid, array, false, false, false, packetToggle());
			}
		} catch (MalformedPacketException e) {
			Lib.debug('l', "Bad Packet (nextPacket) - " + e.toString());
			return null;
		}
		return data;
	}
	public void sendAll(){
		sendLock.acquire();
		//fill the window
		if(resendCounter > resendCounterMax){
			swin.remove();
		}
		MailMessage msg;
		while (swin.size() < windowSize && (!readyToWrite.isEmpty() || state == state.NStateClosing) && (msg = nextPacket()) != null) {
			Lib.debug('l', "filling the window...");
			swin.add(msg);
		}
		if(swin.notempty()){
			swin.access.acquire();
			for (MailMessage m : swin.packets) {
				Lib.debug('l', "Sending packet: " + m);
				NetKernel.postOffice.send(m);
			}
			swin.access.release();
			resendCounter++;
		}
		sendLock.release();
	}
	//Recieve-----------------------------------------
	// this ones going to be a bitch.  Sorry for re-specifying
	// it so many times, but i think i've got it now.
	// First the packet should b "droped(msg)".  it'll
	// return whether the packet is good or not, and do a bit
	// of book-keeping.
	// Then move on to handle the flags
	// then buffer back the packets via "bufferRecieved(msg)
	// the seperation should be helpful so that if we need to
	// change one section, recieve should be largley untouched.
	//================================================
	private boolean dropPacket(MailMessage msg){
		if(msg.connectionid != connectionid && state != state.NStateClosed && state != state.NStateSynSent){
			Lib.debug('l', "-----Not for me: " + msg.connectionid + " isnt " + connectionid);
			return true;
		}
		if(msg.syn && msg.fin && msg.ack){
			//wtf is this? all the flags are set! drop that fscked shit.
			Lib.debug('l', "dropped a fucked up packet");
			return true;
		}
		if(!checkToggle(msg.posToggle)){
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + " - droped an unexpected packet: -- " + msg);
			return true;
		}
		return false;
	}
	public final void recieve(MailMessage msg){
		Lib.debug('l', this.thisMachineNum + ":" + thisPortNum + " - got me a fskin message: -- " + msg);
		recieveLock.acquire();
		if(!dropPacket(msg)){
			if(msg.syn){
				handleSyn(msg);	   //loop throught the
			} else if(msg.fin){
				handleFin(msg);    //flags and find out
			} else if(msg.ack){
				handleAck(msg);    //how they need to be processed
			} else {
				bufferRecieved(msg); //and buffer non-controll packets
			}
		}
		recieveLock.release();
	}
	//bufferRecieved----------------------------------
	// put the packet on the applicable queues
	// if window full, dump it + send ack via postoffice
	// (not the send window!). finaly push the packet
	// onto window
	//================================================
	private void bufferRecieved(MailMessage msg){
		Lib.debug('l', "buffering a packet: " + msg);
		if(!rwin.add(msg)){
			return;
		}
		MailMessage ack;
		try {
			ack = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid, new byte[0], false, true, false, packetToggle());
		} catch (MalformedPacketException e) {
			Lib.debug('l', "Fuck me...");
			return;
		}
		NetKernel.postOffice.send(ack);
	}
	public void handleSyn(MailMessage msg){
		if(msg.ack && state == SocketState.NStateSynSent){ //its a synack
			Lib.debug('l', thisMachineNum + ": Got a SYN ACK packet");
			state = SocketState.NStateEstablished;
			swin.remove();
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + ": Moved to state ESTABLISHED");
			connectionid = msg.connectionid;
			MailMessage ack;
			try {
				ack = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid,
					new byte[0], false, true, false, packetToggle());
			} catch (MalformedPacketException e) {
				Lib.debug('l', "could not respond to synack due to malformed packet; socket remaining open");
				return;
			}
			NetKernel.postOffice.send(ack);
		} else if(!msg.ack && state == SocketState.NStateClosed){ //its a syn
			Lib.debug('l', thisMachineNum + ": Got a SYN packet");
			//get data from the packet
			thatMachineNum = msg.packet.srcLink;
			thatPortNum = msg.srcPort;
			connectionid = NetKernel.postOffice.newConnectionId(thisPortNum);
			MailMessage synack;
			try {
				synack = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid,
					new byte[0], true, true, false, packetToggle());

			} catch (MalformedPacketException e) {
				state = SocketState.NStateClosed;
				Lib.debug('l', "could not respond to ack due to malformed packet; closing socket");
				return;
			}
			swin.add(synack);
			Lib.debug('l', "Synack sent: " + synack);
			state = SocketState.NStateSynAckSent;
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + ": Moved to state SYNACK-SENT");
		}
	}
	public void handleAck(MailMessage msg){
		Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + " - got an ack");
		if(state == state.NStateSynAckSent){
			state = state.NStateEstablished;
			swin.remove();
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + " - Moved to state ESTABLISHED");
		} else if(state == state.NStateFinAckSent){
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + " I lie forgotten...");
			NetKernel.postOffice.forgetSocket(this, thisPortNum);
		} else {
			sendLock.acquire();
			swin.remove();
			sendLock.release();
		}
	}
	public void handleFin(MailMessage msg){
		Lib.debug('l', "Got a motherfucking fin");
		if(state == state.NStateEstablished){
			Lib.debug('l', "OH SHIT! CLOSE DOWN?");
			state = state.NStateClosing;
			try {
				MailMessage ack = new MailMessage(thatMachineNum, thatPortNum, thisMachineNum, thisPortNum, connectionid, new byte[0],
					false, true, true, packetToggle());
				NetKernel.postOffice.send(ack);
			} catch (MalformedPacketException e) {
				Lib.debug('l', "Could not generate finack:" + e.toString());
			}
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + " I lie forgotten...");
			NetKernel.postOffice.forgetSocket(this, thisPortNum);
		}
		if(state == state.NStateClosing && msg.ack){
			Lib.debug('l', "finack");
			Lib.debug('l', thisMachineNum + ":" + thisPortNum + ":" + connectionid + " I lie forgotten...");
			NetKernel.postOffice.forgetSocket(this, thisPortNum);
		}
	}
	//read and write+++++++++++++++++++++++++++++++++++
	// this is the interface to the program
	// read will put packets to the end of outPackets
	// write will get packets from the top of readyPackets
	// it's a fairly simple thing as far as i can tell.
	// look at MailMessage to find out how packets work
	// read - puts byte arrays from ready together to get
	// an array of the requested size and returns that.
	// or just return when you run out of data
	// write - break the mesage up into packets and send
	// put the packets in out pacets etc. etc.
	//=================================================
	public int write(byte bt[], int offset, int requestedSize){
		//check that status of this socket before continuing
		int bytesWriten = 0;
		if(state == state.NStateEstablished){
			for (bytesWriten = offset; bytesWriten < requestedSize - offset; bytesWriten++) {
				readyToWrite.add(bt[bytesWriten]);
			}
			bytesWriten -= offset;
		}
		return bytesWriten;
	}
	public int read(byte bt[], int offset, int requestedSize){
		//check the state of the connection before continuing
		if(state == SocketState.NStateClosing || state == SocketState.NStateClosed){
			return 0;
		}
		//fill the array all fancy-like
		byte[] tbt = new byte[requestedSize];
		int l = rwin.read(tbt, requestedSize);
		System.arraycopy(tbt, 0, bt, offset, bt.length);
		return l;
	}
	public boolean connect(int destMachine, int destPort){
		if(state != state.NStateClosed){
			return false;
		}
		//set up some vars--------------
		thatMachineNum = destMachine;
		thatPortNum = destPort;
		NetKernel.postOffice.rememberSocket(this, thisPortNum);
		//------------------------------
		//send the syn packet-----------
		MailMessage msg;
		try {
			msg = new MailMessage(destMachine, destPort, thisMachineNum, thisPortNum, connectionid, new byte[0], true, false, false, packetToggle());
			System.out.println(msg);
		} catch (MalformedPacketException e) {
			Lib.debug('l', "connect fucked up from a malformed packet");
			return false;
		}
		//swap states-------------------
		int count = 0;
		state = SocketState.NStateSynSent;
		swin.add(msg);
		while (state == SocketState.NStateSynSent && count < NetKernel.maxTimeout) {
			NetKernel.timeout();
			count++;
		}
		//did it work-------------------
		if(state != SocketState.NStateEstablished){
			Lib.debug('l', "Could not establish a connection due to a timeout");
			NetKernel.postOffice.forgetSocket(this, thisPortNum);
		} else {
			Lib.debug('l', "Connection established!");
		}
		return (state == state.NStateEstablished);
	}
	public boolean accept(int _thisPortNum){
		//setup this sockets vars
		thisPortNum = _thisPortNum;
		connectionid = NetKernel.postOffice.newConnectionId(_thisPortNum);
		NetKernel.postOffice.rememberSocket(this, thisPortNum);
		//-----------------------
		//wait for a connection--
		int count = 0;
		while (count < NetKernel.maxTimeout) {
			NetKernel.timeout();
			count++;
		}
		//-----------------------
		//didi it work-----------
		if(state != SocketState.NStateEstablished){
			Lib.debug('l', "Could not establish a connection due to a timeout");
			NetKernel.postOffice.forgetSocket(this, thisPortNum);
		} else {
			Lib.debug('l', "Connection accepted");
		}
		return state == state.NStateSTPrcvd;
		//-----------------------
	}
	//prety much just push a fin packet onto the list and move to closing state
	public void close(){
		state = SocketState.NStateClosing;
	}
	// Just an accessor function I used to find out what the current state
	// of a socket was
	public SocketState GetState(){
		return state;
	}
	//toggle the sent packet... just a bit of magic
	private boolean packetToggle(){
		cToggle = !cToggle;
		return cToggle;
	}
	private boolean checkToggle(boolean magic){
		if(magic == eToggle){
			eToggle = !eToggle;
			return true;
		}
		return false;
	}
}
